# coding: utf-8

from sys import path
import sys
import subprocess
import os
import time
import datetime
import re
import serial
import serial.tools.list_ports
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QComboBox, QPushButton, QTextEdit, QLabel, QLineEdit, QMessageBox, QFileDialog, QHBoxLayout, QTabWidget, QGraphicsView, QGraphicsScene, QGraphicsEllipseItem, QCheckBox
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtCore import Qt, QTimer, QThread, Signal, QObject, QMetaObject
from PySide6.QtGui import QIcon, QPainter, QPen, QColor, QFont, QFontDatabase

from functools import partial

import json_config

import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

def parse_string_to_int_list(string, delimiter=None, column_number=None):

    int_list = []
    
    try:
        original_data_list = string.split(delimiter)

        for data in original_data_list:
            int_list.append(int(data))

    except ValueError:
        int_list = []
        pass
    
    return int_list

class SerialMonitorApp(QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.config = json_config.JsonConfig('config.json')

        self.serial_port = None
        self.selected_port = None
        self.is_connected = False

        self.read_thread = None
        self.serial_reader = None
        self.reconnect_enable = self.config.data["serial_port"]["reconnect"]["enable"]

        self.port_dict = None

        self.log_path = self.config.data["log"]["path"]
        self.log_file = None
        self.log_buffer = None
        self.app_started = False

        self.timer_plot = QTimer(self)
        self.timer_plot.timeout.connect(self.callback_update_plot_tab)
        self.timer_plot.start(1)

        self.timer_update_serial_ports = QTimer(self)
        self.timer_update_serial_ports.timeout.connect(self.callback_update_serial_ports)
        self.timer_update_serial_ports.start(100)

        self.dx = 0
        self.dy = 0
        
        self.np_x = np.array([])
        self.np_y = np.array([])
        
        self.image_buffer = None
        self.image_size = self.config.data["plot"]["Matplotlib"]["image"]["width"] * self.config.data["plot"]["Matplotlib"]["image"]["height"]
        self.flag_image_data = True

        self.plot_brush_color = self.config.data["plot"]["brush_color"]
        self.plot_type = self.config.data["plot"]["plot_type"]
        
        self.log_auto_scroll_enable = self.config.data["log"]["auto_scroll"]
        self.log_script_enable = self.config.data["log"]["script"]["enable"]
        
        if sys.platform.startswith('win'):  # in windows
            self.log_script = self.config.data["log"]["script"]["windows"]
        else:
            self.log_script = self.config.data["log"]["script"]["unix-like"]
        self.plot_dot_disontinuous = self.config.data["plot"]["Dot"]["discontinuous"]
        
        # UI绘制需要在所有实例变量创建后执行
        self.initialize_UI()
        
    def initialize_UI(self):

        self.setWindowTitle("Serial Monitor")
        self.resize(self.config.data["window"]["width"],
                    self.config.data["window"]["height"])

        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        layout = QVBoxLayout(self.central_widget)

        # ------------------ Menu Bar ------------------

        # Serial Port Selection
        self.serial_port_label = QLabel("<b>Serial Port:</b>")
        self.serial_port_combo = QComboBox()
        self.connect_button = QPushButton("Connect")
        self.connect_button.setCheckable(True)
        self.connect_button.setChecked(False)
        self.connect_button.clicked.connect(self.handle_connect_button)

        HL_1 = QHBoxLayout()

        HL_1.addWidget(self.serial_port_label, 1)
        HL_1.addWidget(self.serial_port_combo, 10)
        HL_1.addWidget(self.connect_button, 2)

        HL_1_sub_1 = QHBoxLayout()
        self.serial_baudrate_label = QLabel("<b>Baudrate:</b>")
        self.serial_baudrate_combo = QComboBox()
        self.serial_baudrate_combo.addItems(
            [str(baudrate) for baudrate in self.config.data["serial_port"]["baudrate_list"]])
        self.serial_baudrate_combo.setCurrentText(
            str(self.config.data["serial_port"]["baudrate"]))
        self.serial_baudrate_combo.currentTextChanged.connect(
            self.handle_serial_baudrate_changed)

        self.serial_bytesize_label = QLabel("<b>Bytesize:</b>")
        self.serial_bytesize_combo = QComboBox()
        self.serial_bytesize_combo.addItems(
            [str(bytesize) for bytesize in self.config.data["serial_port"]["bytesize_list"]])
        self.serial_bytesize_combo.setCurrentText(
            str(self.config.data["serial_port"]["bytesize"]))
        self.serial_bytesize_combo.currentTextChanged.connect(
            self.handle_serial_bytesize_changed)

        self.serial_parity_label = QLabel("<b>Parity:</b>")
        self.serial_parity_combo = QComboBox()
        self.serial_parity_combo.addItems(
            [str(parity) for parity in self.config.data["serial_port"]["parity_list"]])
        self.serial_parity_combo.setCurrentText(
            str(self.config.data["serial_port"]["parity"]))
        self.serial_parity_combo.currentTextChanged.connect(
            self.handle_serial_parity_changed)

        self.serial_stopbits_label = QLabel("<b>Stopbits:</b>")
        self.serial_stopbits_combo = QComboBox()
        self.serial_stopbits_combo.addItems(
            [str(stopbits) for stopbits in self.config.data["serial_port"]["stopbits_list"]])
        self.serial_stopbits_combo.setCurrentText(
            str(self.config.data["serial_port"]["stopbits"]))
        self.serial_stopbits_combo.currentTextChanged.connect(
            self.handle_serial_stopbits_changed)

        HL_1_sub_1.addWidget(self.serial_baudrate_label, 1)
        HL_1_sub_1.addWidget(self.serial_baudrate_combo, 8)
        HL_1_sub_1.addWidget(self.serial_bytesize_label, 1)
        HL_1_sub_1.addWidget(self.serial_bytesize_combo, 8)
        HL_1_sub_1.addWidget(self.serial_parity_label, 1)
        HL_1_sub_1.addWidget(self.serial_parity_combo, 8)
        HL_1_sub_1.addWidget(self.serial_stopbits_label, 1)
        HL_1_sub_1.addWidget(self.serial_stopbits_combo, 8)

        layout.addLayout(HL_1)
        layout.addLayout(HL_1_sub_1)

        # -------------------------- Tab Widget -----------------------------

        self.tab_widget = QTabWidget(self.central_widget)

        self.log_page = QWidget()
        self.plot_page = QWidget()
        self.advanced_page = QWidget()

        # add widget on each tab page
        log_page_layout = QVBoxLayout(self.log_page)
        self.log_page.setLayout(log_page_layout)

        self.plot_tab_layout = QVBoxLayout(self.plot_page)
        self.plot_page.setLayout(self.plot_tab_layout)
        
        advanced_page_layout = QVBoxLayout(self.advanced_page)
        self.plot_page.setLayout(advanced_page_layout)

        self.tab_widget.addTab(self.log_page, "Log")
        self.tab_widget.addTab(self.plot_page, "Plot")
        self.tab_widget.addTab(self.advanced_page, "Advanced")

        VL_tab = QVBoxLayout()
        VL_tab.addWidget(self.tab_widget)

        layout.addLayout(VL_tab)

        # -------------------------- Log Display -----------------------------
        self.log_text_edit = QTextEdit()
        self.log_text_edit.setReadOnly(True)
        
        log_prefix_label = QLabel("<b>Prefix:</b>")
        log_prefix_LineEdit = QLineEdit()
        log_prefix_LineEdit.setText(self.config.data["log"]["prefix"])
        log_prefix_LineEdit.textChanged.connect(self.handle_log_prefix_changed)
        
        log_suffix_label = QLabel("<b>Suffix:</b>")
        log_suffix_combo = QComboBox()
        log_suffix_combo.addItems([str(suffix) for suffix in self.config.data["log"]["suffix_list"]])
        log_suffix_combo.setCurrentText(str(self.config.data["log"]["suffix"]))
        log_suffix_combo.currentTextChanged.connect(self.handle_log_suffix_changed)

        available_fonts = QFontDatabase().families(QFontDatabase.Any)

        font_family = self.config.data["log"]["font_family"]
        if font_family in available_fonts:
            font = QFont(font_family, self.config.data["log"]["font_size"])
            self.log_text_edit.setFont(font)
        else:
            QMessageBox.information(self, "ERROR", f"Font family '{font_family}' cannot be found in your system, please check the 'config.json' file.")

        HL1_log_tab_page = QHBoxLayout()
        HL1_log_tab_page.addWidget(log_prefix_label, 1)
        HL1_log_tab_page.addWidget(log_prefix_LineEdit, 10)
        HL1_log_tab_page.addWidget(log_suffix_label, 1)
        HL1_log_tab_page.addWidget(log_suffix_combo, 4)
        log_page_layout.addLayout(HL1_log_tab_page)
        log_page_layout.addWidget(self.log_text_edit)

        # -------------------------- Regex Split -----------------------------
        self.log_path_button = QPushButton("Select Log Path")
        self.log_path_button.clicked.connect(self.handle_log_path_button)
        self.log_path_button.setCheckable(True)
        self.log_path_button.setChecked(False)

        self.regex_label = QLabel("<b>Regex Pattern:</b>")
        self.regex_input = QLineEdit()
        self.regex_input.setText(self.config.data["regex"]["pattern"])

        self.regex_button = QPushButton("Split Logs")
        self.regex_button.clicked.connect(self.handle_regex_button)

        HL_2 = QHBoxLayout()

        HL_2.addWidget(self.log_path_button, 2)
        HL_2.addWidget(self.regex_label, 1)
        HL_2.addWidget(self.regex_input, 8)
        HL_2.addWidget(self.regex_button, 2)

        log_page_layout.addLayout(HL_2)


        # -------------------------- Plot Page -----------------------------
        
        self.flag_plotting = False
        self.flag_plotting_matplotlib_image_enable = False
        self.plot_type_label = QLabel("<b>Plot Type:</b>")
        self.plot_type_combo = QComboBox()
        self.plot_type_combo.addItems(
            [str(plot_type) for plot_type in self.config.data["plot"]["type_list"]])
        self.plot_type_combo.setCurrentText(
            str(self.config.data["plot"]["plot_type"]))
        self.plot_type_combo.currentTextChanged.connect(
            self.handle_plot_type_changed)
        self.plot_page_enable_button = QPushButton("Start to plot")
        self.plot_page_enable_button.setCheckable(True)
        self.plot_page_enable_button.setChecked(False)
        self.plot_page_enable_button.clicked.connect(self.handle_plot_page_enable_button)

        self.view = QGraphicsView(self)
        self.scene = QGraphicsScene(self)
        self.view.setScene(self.scene)

        # 设置画布原点在中心
        self.view.setRenderHint(QPainter.Antialiasing)
        self.view.setSceneRect(self.config.data["plot"]["scene_x"], self.config.data["plot"]["scene_y"], self.config.data["plot"]["scene_width"], self.config.data["plot"]["scene_height"])

        HL_3 = QHBoxLayout()

        HL_3.addWidget(self.plot_type_label, 1)
        HL_3.addWidget(self.plot_type_combo, 8)
        HL_3.addWidget(self.plot_page_enable_button, 3)

        self.plot_tab_layout.addLayout(HL_3)
        self.plot_tab_layout.addWidget(self.view)
        
        # -------------------------- Advanced Page -----------------------------
        
        # WIDGET
        LOG_label = QLabel("<b><center>LOG CONFIGURATION</center></b>")

        VL1_Advanced_page = QVBoxLayout()
        VL1_Advanced_page.setAlignment(Qt.AlignTop)
        
        reconnect_enable_checkbox = QCheckBox("Reconnect enable")
        reconnect_enable_checkbox.setChecked(self.config.data["serial_port"]["reconnect"]["enable"])
        reconnect_enable_checkbox.stateChanged.connect(self.handle_reconnect_enable_changed)
        
        reconnect_keyword_LineEdit = QLineEdit()
        reconnect_keyword_LineEdit.setText(self.config.data["serial_port"]["reconnect"]["keyword"])
        reconnect_keyword_LineEdit.textChanged.connect(self.handle_reconnect_keyword_changed)
        
        auto_scroll_enable_checkbox = QCheckBox("Auto Scroll enable")
        auto_scroll_enable_checkbox.setChecked(self.config.data["log"]["auto_scroll"])
        auto_scroll_enable_checkbox.stateChanged.connect(self.handle_auto_scroll_enable_changed)
        
        log_script_enable_checkbox = QCheckBox("Log script enable")
        log_script_enable_checkbox.setChecked(self.config.data["log"]["script"]["enable"])
        log_script_enable_checkbox.stateChanged.connect(self.handle_log_script_enable_changed)
        
        log_script_LineEdit = QLineEdit()
        log_script_LineEdit.setText(self.log_script)
        log_script_LineEdit.textChanged.connect(self.handle_log_script_changed)
        
        PLOT_label = QLabel("<br><b><center>PLOT CONFIGURATION</center></b>")
        
        dot_plot_discontinuous_checkbox = QCheckBox("Dot plot discontinuous")
        dot_plot_discontinuous_checkbox.setChecked(self.config.data["plot"]["Dot"]["discontinuous"])
        dot_plot_discontinuous_checkbox.stateChanged.connect(self.handle_plot_dot_discontinuous_changed)
        
        matplotlib_type_label = QLabel("<b>Matplotlib Type:</b>")
        matplotlib_type_combo = QComboBox()
        matplotlib_type_combo.addItems([str(matplotlib_type) for matplotlib_type in self.config.data["plot"]["Matplotlib"]["type_list"]])
        matplotlib_type_combo.setCurrentText(str(self.config.data["plot"]["Matplotlib"]["type"]))
        matplotlib_type_combo.currentTextChanged.connect(self.handle_plot_matplotlib_type_changed)
        
        DATA_PARSE_label = QLabel("<br><b><center>DATA PARSE</center></b>")
        data_parse_column_number_label = QLabel("<b>Column Number:</b>")
        data_parse_column_number_LineEdit = QLineEdit()
        data_parse_column_number_LineEdit.setText(str(self.config.data["log"]["csv_data_parse"]["column_number"]))
        data_parse_column_number_LineEdit.textChanged.connect(self.handle_data_parse_column_number_changed)
        
        data_parse_target_column_label = QLabel("<b>Target Column:</b>")
        data_parse_target_column_LineEdit = QLineEdit()
        data_parse_target_column_LineEdit.setText(str(self.config.data["log"]["csv_data_parse"]["target_column"]))
        data_parse_target_column_LineEdit.textChanged.connect(self.handle_data_parse_target_column_changed)
        
        data_parse_delimiter_label = QLabel("<b>Delimiter:</b>")
        data_parse_delimiter_LineEdit = QLineEdit()
        data_parse_delimiter_LineEdit.setText(str(self.config.data["log"]["csv_data_parse"]["delimiter"]))
        data_parse_delimiter_LineEdit.textChanged.connect(self.handle_data_parse_target_column_changed)
        
        # LAYOUT
        
        HL0_Advanced_page = QHBoxLayout()
        HL0_Advanced_page.addWidget(LOG_label)
        
        HL1_Advanced_page = QHBoxLayout()
        HL1_Advanced_page.addWidget(reconnect_enable_checkbox, 1)
        HL1_Advanced_page.addWidget(reconnect_keyword_LineEdit, 8)
        
        HL2_Advanced_page = QHBoxLayout()
        HL2_Advanced_page.addWidget(log_script_enable_checkbox, 1)
        HL2_Advanced_page.addWidget(log_script_LineEdit, 8)
        
        HL3_Advanced_page = QHBoxLayout()
        HL3_Advanced_page.addWidget(auto_scroll_enable_checkbox, 1)
        
        HL4_Advanced_page = QHBoxLayout()
        HL4_Advanced_page.addWidget(PLOT_label)
        
        HL5_Advanced_page = QHBoxLayout()
        HL5_Advanced_page.addWidget(dot_plot_discontinuous_checkbox, 1)
        HL5_Advanced_page.addWidget(matplotlib_type_label, 1)
        HL5_Advanced_page.addWidget(matplotlib_type_combo, 4)

        HL6_Advanced_page = QHBoxLayout()
        HL6_Advanced_page.addWidget(DATA_PARSE_label)
        
        HL7_Advanced_page = QHBoxLayout()
        HL7_Advanced_page.addWidget(data_parse_column_number_label, 1)
        HL7_Advanced_page.addWidget(data_parse_column_number_LineEdit, 7)
        HL7_Advanced_page.addWidget(data_parse_target_column_label, 1)
        HL7_Advanced_page.addWidget(data_parse_target_column_LineEdit, 7)
        HL7_Advanced_page.addWidget(data_parse_delimiter_label, 1)
        HL7_Advanced_page.addWidget(data_parse_delimiter_LineEdit, 7)
        
        VL1_Advanced_page.addLayout(HL0_Advanced_page)
        VL1_Advanced_page.addLayout(HL1_Advanced_page)
        VL1_Advanced_page.addLayout(HL2_Advanced_page)
        VL1_Advanced_page.addLayout(HL3_Advanced_page)
        VL1_Advanced_page.addLayout(HL4_Advanced_page)
        VL1_Advanced_page.addLayout(HL5_Advanced_page)
        VL1_Advanced_page.addLayout(HL6_Advanced_page)
        VL1_Advanced_page.addLayout(HL7_Advanced_page)
        
        advanced_page_layout.addLayout(VL1_Advanced_page)
        

    def callback_update_serial_ports(self):
        
        ports = serial.tools.list_ports.comports()  # 获取当前可用的串口列表

        # Create a dictionary to store port names and their corresponding devices
        port_dict = {}
        for port in ports:
            port_dict[port.device] = port
        
        # 比较 self.port_dict 和 port_dict，如果不一致，则更新 self.port_dict
        if self.port_dict != port_dict:
            self.port_dict = port_dict
        else:
            return

        self.serial_port_combo.clear()
        if self.config.data["serial_port"]["selected_port"] in port_dict.keys():    
            selected_port_name = port_dict[self.config.data["serial_port"]["selected_port"]].name
            selected_port_description = port_dict[self.config.data["serial_port"]["selected_port"]].description
            self.serial_port_combo.addItem(f"{selected_port_name} - {selected_port_description}")
        else:
            self.config.data["serial_port"]["selected_port"] = ""
            self.config.save_data()

        for port_device, port in port_dict.items():
            if port_device != self.config.data["serial_port"]["selected_port"]:
                self.serial_port_combo.addItem(
                    f"{port.name} - {port.description}")

    def handle_connect_button(self):

        if not self.is_connected:
            
            # 如果 log_path 不为 ""，但os检查后不存在，则自动创建该路径
            # 否则，则弹出文件选择对话框，让用户选择日志保存路径，并且将该路径保存至json文件中
            if self.log_path != "" and not os.path.exists(self.log_path):
                try:
                   os.mkdir(self.log_path)
                except OSError as e:
                    QMessageBox.information(
                        self, "ERROR", "Please press 'Select Log Path' button to select a directory to save logs first.")
                    self.connect_button.setChecked(False)
                    return
                        
            elif self.log_path == "":
                self.log_path = QFileDialog.getExistingDirectory(
                    self, "Select Directory", ".")
                if self.log_path:
                    self.config.data["log"]["path"] = self.log_path
                    self.config.save_data()
                else:
                    QMessageBox.information(
                        self, "ERROR", "Please select a directory to save logs first.")
                    return

            
            if self.serial_port_combo.count() == 0:
                QMessageBox.warning(
                    self, "Serial Port Error", "No serial port found.")
                self.connect_button.setChecked(False)
                return
            
            selected_port_text = self.serial_port_combo.currentText()

            if sys.platform.startswith('win'):  # in windows 
                selected_port = selected_port_text.split(
                )[-1].replace("(", "").replace(")", "")  # 反向切分获取 port.device
            else:
                # 正向切分 获取 ttyUSB0
                parts = selected_port_text.replace(" ","").split("-")
                selected_port = "/dev/"+parts[0].replace(" ","")

            try:
                self.serial_port = serial.Serial(
                    selected_port,
                    baudrate=self.config.data["serial_port"]["baudrate"],
                    bytesize=self.config.data["serial_port"]["bytesize"],
                    parity=self.config.data["serial_port"]["parity"],
                    stopbits=self.config.data["serial_port"]["stopbits"],
                )
                self.config.data["serial_port"]["selected_port"] = selected_port
                self.config.save_data()  # 将其存储至json文件中

                self.is_connected = True
                self.connect_button.setText("Disconnect")
                self.log_text_edit.clear()

                self.log_buffer = self.config.data["log"]["prefix"] + "_" +\
                    datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + self.config.data["log"]["suffix"]
                
                # 将 log_path 添加至 log_buffer 前面，以便后续使用
                self.log_buffer = self.log_path + "/" + self.log_buffer

                # Create new QThread(s)
                self.read_thread = QThread(self)
                self.serial_reader = SerialReader(self.serial_port, column_number=self.config.data["log"]["csv_data_parse"]["column_number"], target_column=self.config.data["log"]["csv_data_parse"]["target_column"], delimiter=self.config.data["log"]["csv_data_parse"]["delimiter"])
                self.serial_reader.dataReceived.connect(self.callback_append_to_log)
                self.serial_reader.showError.connect(self.show_error_message)
                self.serial_reader.reconnect.connect(self.callback_check_reconnect_keyword)
                self.serial_reader.moveToThread(self.read_thread)

                self.read_thread.finished.connect(self.read_thread.deleteLater)
                self.read_thread.finished.connect(
                    self.serial_reader.deleteLater)
                self.read_thread.started.connect(self.serial_reader.start)

                self.read_thread.start()

            except serial.SerialException as e:
                QMessageBox.warning(self, "Serial Port Error", str(e))
        else:
            self.is_connected = False
            self.log_file = None
            self.app_started = False

            if self.flag_plotting == True:
                self.flag_plotting = False
                self.scene.clear()
                self.plot_page_enable_button.setText("Start to plot")
                self.plot_page_enable_button.setChecked(False)

            if self.serial_port and self.serial_port.is_open:
                self.serial_reader.stop()  # 停止读取线程
                self.serial_port = None
            if self.read_thread is not None:
                self.read_thread.quit()
                self.read_thread.wait()
            self.connect_button.setText("Connect")


    def closeEvent(self, event):
        # Properly release the serial port and stop the thread when the application is closed
        if self.is_connected:
            self.is_connected = False
            if self.serial_port and self.serial_port.is_open:
                self.serial_reader.stop()  # 停止读取线程
                self.serial_port = None
            if self.read_thread is not None and self.read_thread.isRunning():
                self.read_thread.quit()
                self.read_thread.wait()
                self.read_thread = None
        super().closeEvent(event)


    def callback_save_log_buffer(self, data_str):
        # 如果文件不存在，则创建即可
        if not os.path.exists(self.log_buffer):
            with open(self.log_buffer, "w") as file:
                pass

        with open(self.log_buffer, "a") as file:
            file.write(data_str)
            file.write("\n")

    def callback_append_to_log(self, data_str):
        scroll_bar = self.log_text_edit.verticalScrollBar()
        current_scroll_position = scroll_bar.value()

        self.log_text_edit.append(data_str)
        self.callback_save_log_buffer(data_str)

        # 将滚动位置滚动到文本框底部，以保持下拉
        if self.log_auto_scroll_enable:
            scroll_bar.setValue(scroll_bar.maximum())
        else:
            scroll_bar.setValue(current_scroll_position)

    def handle_regex_button(self):

        # 如果已经设置过日志保存路径，则直接使用该路径，否则弹出文件选择对话框
        if self.config.data["regex"]["split_log_path"] == "":
            split_log_path = QFileDialog.getExistingDirectory(
                self, "Select Directory", ".")
        else:
            split_log_path = self.config.data["regex"]["split_log_path"]
            split_log_path = QFileDialog.getExistingDirectory(
                self, "Select Directory", split_log_path)

        if split_log_path:
            self.config.data["regex"]["split_log_path"] = split_log_path
            self.config.save_data()
        else:
            QMessageBox.information(
                self, "ERROR", "Please select a directory to save logs.")
            return

        pattern_str = self.regex_input.text().strip()
        if not pattern_str:
            QMessageBox.information(
                self, "ERROR", "Please input regex pattern.")
            return

        # 使用 re.DOTALL 来匹配包含换行符的多行日志块
        pattern = re.compile(pattern_str, re.DOTALL)

        matches = pattern.findall(self.log_text_edit.toPlainText())

        if matches:
            timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
            os.mkdir(split_log_path + "/" + timestamp)

            # 将可用的正则表达式保存至json文件中
            self.config.data["regex"]["pattern"] = pattern_str
            self.config.save_data()

            split_number = 0
            for index, match in enumerate(matches, start=1):
                log_file_name = f"matched_log_{index}.log"
                with open(split_log_path + "/" + timestamp + "/" + log_file_name, "w") as file:
                    file.write(match)
                split_number += 1

            self.log_text_edit.clear()
            QMessageBox.information(
                self, "Regex Match", f"Split Number: {split_number}")
        else:
            QMessageBox.information(self, "Regex Match", "No matches found.")

    def handle_serial_baudrate_changed(self, baudrate):

        if self.serial_port and self.serial_port.is_open:
            self.serial_port.baudrate = int(baudrate)

        self.config.data["serial_port"]["baudrate"] = int(baudrate)
        self.config.save_data()

    def handle_serial_bytesize_changed(self, bytesize):
            
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.bytesize = int(bytesize)
    
        self.config.data["serial_port"]["bytesize"] = int(bytesize)
        self.config.save_data()

    def handle_serial_parity_changed(self, parity):

        if self.serial_port and self.serial_port.is_open:
            self.serial_port.parity = parity

        self.config.data["serial_port"]["parity"] = parity
        self.config.save_data()

    def handle_serial_stopbits_changed(self, stopbits):

        if self.serial_port and self.serial_port.is_open:
            self.serial_port.stopbits = int(stopbits)

        self.config.data["serial_port"]["stopbits"] = int(stopbits)
        self.config.save_data()

    def handle_plot_page_enable_button(self):

        if self.flag_plotting == False:
            
            if self.is_connected == False:
                QMessageBox.information(self, "ERROR", "Please connect to a serial port first.")
                self.plot_page_enable_button.setChecked(False)
                return
            else:

                if self.plot_type == "Dot":
                    self.x = 0
                    self.y = 0
                    
                    self.handle_plot_type_changed("Dot")
                    
                elif self.plot_type == "Matplotlib":
                    
                    self.x = 0
                    self.y = 0
                    
                    self.handle_plot_type_changed("Matplotlib")
                    
                self.flag_plotting = True
                self.plot_page_enable_button.setText("Stop plotting")

        else:
            self.flag_plotting = False
            if self.plot_type == "Dot":
                self.scene.clear()
            self.plot_page_enable_button.setText("Start to plot")

    def callback_update_plot_dot(self):

        if self.flag_plotting == False:
            return

        if self.dx == 0 and self.dy == 0:
            return
        
        self.x += self.dx
        self.y += self.dy

        self.dx = 0
        self.dy = 0

        if self.x >= self.config.data["plot"]["scene_width"] - self.config.data["plot"]["margin_width"]:  # 正整数
            self.x = self.config.data["plot"]["scene_width"] - self.config.data["plot"]["margin_width"]
        elif self.x <= - (self.config.data["plot"]["scene_width"] - self.config.data["plot"]["margin_width"]):  # 负整数
            self.x = - ( self.config.data["plot"]["scene_width"] - self.config.data["plot"]["margin_width"])

        if self.y >= self.config.data["plot"]["scene_height"] - self.config.data["plot"]["margin_height"]:  # 正整数
            self.y = self.config.data["plot"]["scene_height"] - self.config.data["plot"]["margin_height"]
        elif self.y <= - (self.config.data["plot"]["scene_height"] - self.config.data["plot"]["margin_height"]):  # 负整数
            self.y = - (self.config.data["plot"]["scene_height"] - self.config.data["plot"]["margin_height"])

        if self.plot_dot_disontinuous:
            self.scene.clear()

        point_item = QGraphicsEllipseItem(self.x - 5, self.y - 5, 10, 10)
        point_item.setBrush(QColor(self.plot_brush_color))

        pen = QPen(Qt.transparent)  # 将边框颜色设置为透明
        point_item.setPen(pen)  # 应用透明的边框颜色

        self.scene.addItem(point_item)
        
    def callback_update_plot_matplotlib(self):
        
        if self.config.data["plot"]["Matplotlib"]["image"]["enable"]:
            
            if len(self.image_buffer) == self.image_size :
                
                image_buffer_int = [int(hex_str, 16) for hex_str in self.image_buffer]
                self.image_buffer = []
                self.flag_image_data = True
                
                np_image = np.array(image_buffer_int, dtype=np.uint8)
                np_image = np_image.reshape(self.config.data["plot"]["Matplotlib"]["image"]["height"], self.config.data["plot"]["Matplotlib"]["image"]["width"])
                
                if self.config.data["plot"]["Matplotlib"]["image"]["save_enable"] == True:
                    if not os.path.exists(self.config.data["plot"]["Matplotlib"]["image"]["save_path"]):
                        os.mkdir(self.config.data["plot"]["Matplotlib"]["image"]["save_path"])
                    
                    image_file_name = self.config.data["plot"]["Matplotlib"]["image"]["save_path"] + f"/{time.time() * 1e6}.raw"
                    # while os.path.exists(image_file_name + ".raw"):
                    #     image_file_name = image_file_name + "_1"
                            
                    # image_file_name = image_file_name + ".raw"
                    np_image.tofile(image_file_name)
                else:
                    self.ax.imshow(np_image, cmap='gray')
                    self.canvas.draw()
            
        elif self.config.data["plot"]["Matplotlib"]["curve"]["enable"]:
        
            data_number = self.config.data["plot"]["Matplotlib"]["curve"]["data_number"]
            
            if self.config.data["plot"]["Matplotlib"]["curve"]["sum_up"]:
                self.x += self.dx
                self.y += self.dy
            else:
                self.x = self.dx
                self.y = self.dy
            
            self.dx = 0
            self.dy = 0
            
            self.np_x = np.append(self.np_x, self.x)
            self.np_y = np.append(self.np_y, self.y)
            
            if len(self.np_x) > data_number:
                np_x = self.np_x[-data_number:]
                np_y = self.np_y[-data_number:]
                self.ax.clear()  
                self.ax.plot(np_x, color=self.plot_brush_color)
            else:
                self.ax.plot(self.np_x, color=self.plot_brush_color)
            
            self.canvas.draw()

    def callback_update_plot_tab(self):
        
        if self.flag_plotting == False:
            return
        
        if (self.dx == 0 and self.dy == 0) and self.image_buffer is None:
            return
        
        if self.plot_type == "Dot":
            self.callback_update_plot_dot()
        elif self.plot_type == "Matplotlib":
            self.callback_update_plot_matplotlib()
        else:
            pass

    def handle_plot_type_changed(self, plot_type):
        
        # self.canvas 是 matplotlib 的画布
        # self.view 是 QGraphicsView
        
        # 改变画布前，先检查 self.plot_page_layout 是否已存在任何 widget，是的话就删除该 widget
        
        if self.flag_plotting == True:
            QMessageBox.warning(self, "ERROR", "Change plot type failed, please stop plotting first!")
            return
        
        if self.plot_tab_layout.count() > 1:
            self.plot_tab_layout.itemAt(1).widget().deleteLater()
        
        if plot_type == "Dot":
            
            self.flag_plotting_matplotlib_image_enable = False

            self.view = QGraphicsView(self)
            self.scene = QGraphicsScene(self)
            self.view.setScene(self.scene)

            # 设置画布原点在中心
            self.view.setRenderHint(QPainter.Antialiasing)
            self.view.setSceneRect(self.config.data["plot"]["scene_x"], self.config.data["plot"]["scene_y"], self.config.data["plot"]["scene_width"], self.config.data["plot"]["scene_height"])
            
            self.plot_tab_layout.addWidget(self.view)

        elif plot_type == "Matplotlib":

            if self.config.data["plot"]["Matplotlib"]["image"]["enable"]:
                
                self.flag_plotting_matplotlib_image_enable = True
            
                self.figure = Figure(figsize=(self.config.data["plot"]["Matplotlib"]["image"]["width"], self.config.data["plot"]["Matplotlib"]["image"]["height"]), dpi=100)    
                self.canvas = FigureCanvas(self.figure)
                self.ax = self.figure.add_subplot(111)
                
                self.plot_tab_layout.addWidget(self.canvas)
                
                default_image = self.config.data["plot"]["Matplotlib"]["image"]["default_image"]
                data_type = self.config.data["plot"]["Matplotlib"]["image"]["data_type"]
                np_image = None
                
                if default_image != "":
                    
                    np_image = np.array([int(hex_str, 16) for hex_str in default_image], dtype=np.uint8)
                    np_image = np_image.reshape(self.config.data["plot"]["Matplotlib"]["image"]["height"], self.config.data["plot"]["Matplotlib"]["image"]["width"])
                
                self.ax.imshow(np_image, cmap='gray')
                
                self.ax.axis('off')

            elif self.config.data["plot"]["Matplotlib"]["curve"]["enable"]:
                
                self.flag_plotting_matplotlib_image_enable = False

                self.figure = Figure(dpi=100)
                self.canvas = FigureCanvas(self.figure)
                
                self.plot_tab_layout.addWidget(self.canvas)

                self.ax = self.figure.add_subplot(111)
                self.ax.set_xlim(0, self.config.data["plot"]["Matplotlib"]["curve"]["data_number"])

        self.plot_type = plot_type
        self.config.data["plot"]["plot_type"] = plot_type
        self.config.save_data()
        
    def handle_log_path_button(self):
        
        log_path = None

        if self.config.data["log"]["path"] == "":
            log_path = QFileDialog.getExistingDirectory(
                self, "Select Directory", ".")
        else:
            log_path = self.config.data["log"]["path"]
            log_path = QFileDialog.getExistingDirectory(
                self, "Select Directory", log_path)

        # a new path selected
        if log_path:
            self.config.data["log"]["path"] = log_path
            self.config.save_data()
            self.log_path = log_path
            
        else:
            
            if self.log_path == "":
                QMessageBox.information(
                    self, "ERROR", "Please select a directory to save logs.")

        self.log_path_button.setChecked(False)

    def show_error_message(self, title, message):
        
        QMessageBox.information(self, title, message)
        return
    
    def callback_check_reconnect_keyword(self, data_str):
        
        if self.reconnect_enable:
        
            if self.config.data["serial_port"]["reconnect"]["keyword"] in data_str:
                self.handle_connect_button()
                
                if self.log_script_enable and self.log_script != "":
                    
                    # 预处理 log_path
                    if sys.platform.startswith('win'):
                        log_path = self.log_path.replace('/', "\\")
                        execute_command = fr"{self.log_script} {log_path}"
                    else:
                        execute_command = fr"{self.log_script} {self.log_path}"
                    
                    # 执行 log_script
                    try:
                        subprocess.run(execute_command, capture_output=True, text=True, check=True, shell=True)
                    except subprocess.CalledProcessError as e:
                        QMessageBox.information(self, "LOG SCRIPT ERROR", f"Please check the log script.\n{e.stderr}")
                        return
                    
                self.handle_connect_button()
        else:
            return
    
    def handle_reconnect_enable_changed(self, state):
        
        if state == 2:
            self.config.data["serial_port"]["reconnect"]["enable"] = True
        else:
            self.config.data["serial_port"]["reconnect"]["enable"] = False
        self.config.save_data()
        
        self.reconnect_enable = self.config.data["serial_port"]["reconnect"]["enable"]
    
    def handle_reconnect_keyword_changed(self, keyword):
            
        self.config.data["serial_port"]["reconnect"]["keyword"] = keyword
        self.config.save_data()

    def handle_log_prefix_changed(self, log_prefix):
        
        self.config.data["log"]["prefix"] = log_prefix
        self.config.save_data()
    
    def handle_auto_scroll_enable_changed(self, state):
        
        if state == 2:  # 2 means checked
            self.config.data["log"]["auto_scroll"] = True
        else:
            self.config.data["log"]["auto_scroll"] = False
        self.config.save_data()
        
        self.log_auto_scroll_enable = self.config.data["log"]["auto_scroll"]
        
    def handle_log_script_enable_changed(self, state):
        
        if state == 2:
            self.config.data["log"]["script"]["enable"] = True
        else:
            self.config.data["log"]["script"]["enable"] = False
        self.config.save_data()
        
        self.log_script_enable = self.config.data["log"]["script"]["enable"]
        
    def handle_log_script_changed(self, log_script):
        
        if sys.platform.startswith('win'):
            self.config.data["log"]["script"]["windows"] = log_script
        else:
            self.config.data["log"]["script"]["unix-like"] = log_script
        self.config.save_data()
        
        self.log_script = log_script
    
    def handle_log_suffix_changed(self, log_suffix):
        
        self.log_suffix = log_suffix
        self.config.data["log"]["suffix"] = log_suffix
        self.config.save_data()
    
    def handle_plot_dot_discontinuous_changed(self, state):
    
        if state == 2:
            self.config.data["plot"]["Dot"]["discontinuous"] = True
        else:
            self.config.data["plot"]["Dot"]["discontinuous"] = False
        self.config.save_data()
        
        self.plot_dot_disontinuous = self.config.data["plot"]["Dot"]["discontinuous"]
        
    def handle_data_parse_column_number_changed(self, column_number):
        
        self.config.data["log"]["csv_data_parse"]["column_number"] = int(column_number)
        self.config.save_data()
        
    def handle_data_parse_target_column_changed(self, target_column):
        
        self.config.data["log"]["csv_data_parse"]["target_column"] = int(target_column)
        self.config.save_data()
        
    def handle_data_parse_delimiter_changed(self, delimiter):
            
        self.config.data["log"]["csv_data_parse"]["delimiter"] = delimiter
        self.config.save_data()
        
    def handle_plot_matplotlib_type_changed(self, matplotlib_type):
        
        print(matplotlib_type)
        if matplotlib_type == "image":
            self.config.data["plot"]["Matplotlib"]["image"]["enable"] = True
            self.config.data["plot"]["Matplotlib"]["curve"]["enable"] = False
            
        elif matplotlib_type == "curve":
            self.config.data["plot"]["Matplotlib"]["image"]["enable"] = False
            self.config.data["plot"]["Matplotlib"]["curve"]["enable"] = True
            
        self.config.data["plot"]["Matplotlib"]["type"] = matplotlib_type
        
        self.config.save_data()
    
class SerialReader(QObject):
    dataReceived = Signal(str)
    showError = Signal(str, str)
    reconnect = Signal(str)

    def __init__(self, serial_port, column_number=None, target_column=None, delimiter=None):
        super().__init__()
        self.serial_port = serial_port
        self.line_buffer = b""
        self.is_running = True
        
        if column_number is None:
            column_number = 2
        if target_column is None:
            target_column = [0,1]
            
        self.column_number = column_number
        self.target_column = target_column
        self.delimiter = delimiter

    def run(self):
        while self.is_running :
            try:
                if self.serial_port.is_open:
                    if self.serial_port.in_waiting:
                        data = self.serial_port.readline(self.serial_port.in_waiting)
                        self.line_buffer += data
                        while b"\n" in self.line_buffer:
                            data, self.line_buffer = self.line_buffer.split(b"\n", 1)
                            data_str = data.decode('utf-8', errors='replace').strip() # 解码错误时仍显示数据，使用 `replace` 作为错误处理策略，无法解码的字节将别替换为 Unicode 代替字符（U+FFFD）
                            self.dataReceived.emit(data_str)
                            
                            # print(data_str, window.flag_plotting_matplotlib_image_enable)
                            if '---' in data_str and window.flag_plotting_matplotlib_image_enable is True:
                                window.flag_image_data = True
                                window.image_buffer = []
                            
                            # 解析数据至整形
                        
                            if window.flag_plotting == True:
                                if window.flag_image_data is True:
                                    image_data_str = data_str.replace('---', '')
                                        # 检测到 '---\n' 后，说明下一行开始是图像数据，采集任意数量的图像数据，数据间以 ',' 作为分割符，并将其保存至 self.image_buffer 中
                                    if len(window.image_buffer) < window.image_size:
                                        image_data_str = image_data_str.replace('\n', ',')
                                        for pixel in image_data_str.split(','):
                                            if pixel != '':
                                                window.image_buffer.append(pixel)
                                                
                                        if len(window.image_buffer) >= window.image_size:
                                            # window.flag_image_data = False
                                            # pass
                                            print(len(window.image_buffer))
                                            
                                else:
                                    data_list = parse_string_to_int_list(data_str, delimiter=self.delimiter)
                                    if len(data_list) >= self.column_number:
                                        window.dx = data_list[self.target_column[0]]
                                        window.dy = data_list[self.target_column[1]]
                            else:
                                pass

                            # 判断数据中是否含有对应重新连接的关键字
                            self.reconnect.emit(data_str)
                else:
                    raise serial.SerialException("Serial Port Error")
            except serial.SerialException as e:
                self.stop()
                self.showError.emit("Serial Port Error", "Please check your serial port.")
            except OSError as e:
                self.stop()
                self.showError.emit("Serial Port Error", f"I/O error({e.errno}): {e.strerror}")
            except Exception as e:
                self.stop()
                self.showError.emit("Serial Port Error", str(e))
            finally:
                pass    
            

    def start(self):
        self.run()

    def stop(self):
        self.is_running = False
        if self.serial_port.is_open:
            self.serial_port.close()
            self.serial_port = None

def resouse_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    logo_path = resouse_path("logo.png")
    app.setWindowIcon(QIcon(logo_path))
    window = SerialMonitorApp()
    window.show()
    sys.exit(app.exec())
